package com.nado.extensions.excel;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;

import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFDataFormat;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.hssf.util.Region;

/**
 * @author NHN
 * 
 * 자동으로 데이터를 엑셀로 만들때 편리하게 하기 위해 간단하게 자동으로 엑셀 구조를 생성하면서<br>
 * 데이터를 만들 수 있게 도와주는 클래스입니다.<br>
 * 만들고자 하는 엑셀이 있을 경우 이 클래스를 상속하여 이 클래스가 제공하는 간단한 추가 구조를 사용할 수 있습니다.<br>
 * <br>
 * 이 클래스는 엑셀 2003까지의 파일 규격에 따라 한 시트당 65535 rows 데이터를 255개의 시트까지 자동으로 생성하여<br>
 * 더해갈 수 있습니다. 즉, 이 클래스에서 만들어주는 엑셀은 최대 65535 * 255 = 16,711,425 rows, 약 1670만<br>
 * 라인의 데이터가 생성됩니다.<br>
 * 최대 sheet가 255가 되면 더이상 생성을 못하게 되므로 Exception을 던지게 합니다.<br>
 * <br> (<strong>주의!! 예외를 던지지 않도록 수정할 경우 시스템에 큰 메모리 부담이나 에러가 발생할 수 있으므로<br>
 * 이 부분을 수정하지 말아주세요.</strong>)<br>
 * <br>
 * 또한 이 클래스는 엑셀에서 줄 수 있는 간단한 서식을 몇가지 제공합니다.<br>
 * createCell() 함수에서 미리 지정한 몇가지 서식에 대한 상수를 기술함으로써 숫자나 백분율을 표시할 수 있습니다.<br>
 * 커스텀 서식이 필요한 상황이 있을 수 있기 때문에 이 클래스의 대부분의 createXXX 메소드는 해당 객체를 생성 후<br>
 * 리턴하고 있습니다. 상속받은 클래스에서 변수로 받아 새로운 서식을 구현하시면 됩니다.<br>
 * <br>
 * 이 클래스는 apache POI 3.0 버전을 기반으로 만들어졌습니다.<br>
 * 버전이 다른 패키지를 쓰는 다른 프로젝트에 복사해서 사용하고자 할때 버전별 차이점을 찾아 구현하시기 바랍니다.<br> *
 */
public abstract class AbstractPOIExcel {
	/**
	 * excel의 sheet에서 입력가능한 최대 줄 수. (excel 2003 기준)
	 */
	public static final int EXCEL_MAX_ROW = 65535;

	/**
	 * excel에서 생성가능한 최대 sheet 수. (excel 2003 기준)
	 */
	public static final int EXCEL_MAX_SHEET = 255;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 일반 표시 형식으로 25% 회색 바탕에 검은색 실선이 쳐진 셀로
	 * 정중앙 정렬이 되어 있다.
	 */
	public static final int PRESET_STYLE_TEXT_HEADER = 801001;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 일반 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀.
	 */
	public static final int PRESET_STYLE_TEXT_NORMAL = 801101;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 일반 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀로 왼쪽
	 * 정렬로 되어 있다.
	 */
	public static final int PRESET_STYLE_TEXT_CENTER = PRESET_STYLE_TEXT_NORMAL + 1;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 일반 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀로 오른쪽
	 * 정렬로 되어 있다.
	 */
	public static final int PRESET_STYLE_TEXT_RIGHT = PRESET_STYLE_TEXT_NORMAL + 2;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 숫자 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀로 오른쪽
	 * 정렬로 되어 있다. 1000단위 구분 기호인 쉼표(,)가 적용되어 있음.
	 */
	public static final int PRESET_STYLE_NUMBER_NORMAL = 801201;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 숫자 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀로 오른쪽
	 * 정렬로 되어 있다. 1000단위 구분 기호인 쉼표(,)가 적용되어 있음. 소수점 1자리까지 표현가능.
	 */
	public static final int PRESET_STYLE_NUMBER_1DIGIT = PRESET_STYLE_NUMBER_NORMAL + 1;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 숫자 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀로 오른쪽
	 * 정렬로 되어 있다. 1000단위 구분 기호인 쉼표(,)가 적용되어 있음. 소수점 2자리까지 표현가능.
	 */
	public static final int PRESET_STYLE_NUMBER_2DIGIT = PRESET_STYLE_NUMBER_NORMAL + 2;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 백분율 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀로 오른쪽
	 * 정렬로 되어 있다.
	 */
	public static final int PRESET_STYLE_PERCENT_NORMAL = 801301;

	/**
	 * AbstractPOIExcel에서 제공하는 셀 서식.<br/> 백분율 표시 형식으로 투명 바탕에 검은색 실선이 쳐진 셀로 오른쪽
	 * 정렬로 되어 있다. 소수점 2자리까지 표현가능.
	 */
	public static final int PRESET_STYLE_PERCENT_2DIGIT = PRESET_STYLE_PERCENT_NORMAL + 1;

	/**
	 * cell의 가로 크기를 설정할 때 사용하는 단위.<br/> POI 패키지에서 내부적으로 쓰는 단위로 크기를 설정할 수 있도록 제공
	 */
	public static final int WIDTH_UNIT_POI_DEFAULT = 900001;

	/**
	 * cell의 가로 크기를 설정할 때 사용하는 단위.<br/> pixel단위로 크기를 설정할 수 있도록 제공.<br/> POI
	 * 패키지에서 내부적으로 쓰는 단위는 사람이 인식하기 힘들어 AbstractPOIExcel에서 내부적으로 변환계산하여 준다.
	 */
	public static final int WIDTH_UNIT_PIXEL = 900002;

	/**
	 * 하나의 excel 파일에 대한 POI 구현체를 다루는 reference
	 */
	private HSSFWorkbook workBook = null;

	/**
	 * 현재 다루고 있는 sheet에 대한 reference
	 */
	private HSSFSheet currentSheet = null;

	/**
	 * 현재 다루고 있는 row에 대한 reference
	 */
	private HSSFRow currentRow = null;

	/**
	 * 현재 작업하고 있는 row의 index값.
	 */
	private int rowIndexAutoIncrease = -1;

	/**
	 * 현재 작업하고 있는 sheet의 index값.
	 */
	private int sheetIndexAutoIncrease = 0;

	/**
	 * workbook의 내용을 outs에 출력
	 * 
	 * @param outs - BufferedOutputStream
	 */
	public void write(BufferedOutputStream outs) {
		try {
			workBook.write(outs);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * @return - HSSFWorkbook
	 */
	public HSSFWorkbook getWorkBook() {
		return this.workBook;
	}

	/**
	 * Excel의 Header 정보를 생성
	 * 
	 * @throws Exception - java.lang.Exception
	 */
	abstract protected void makeHeaderRows() throws Exception;

	/**
	 * Excel의 Body 정보를 생성
	 * 
	 * @throws Exception - java.lang.Exception
	 */
	abstract protected void makeBodyRows() throws Exception;

	/**
	 * Cell의 크기를 정의
	 * 
	 * @throws Exception - java.lang.Exception
	 */
	abstract protected void setSizeCells() throws Exception;

	/**
	 * Cell간 병합을 정의
	 * 
	 * @throws Exception - java.lang.Exception
	 */
	abstract protected void mergeCells() throws Exception;

	/**
	 * 하나의 Row를 생성합니다.
	 * 
	 * @param values - Object
	 * @throws Exception - java.lang.Exception
	 */
	abstract protected void makeOneRow(Object... values) throws Exception;

	/**
	 * Excel 파일 객체를 생성합니다.
	 * 
	 * @param sheetName - String
	 * @param listCount - int
	 * @throws Exception - java.lang.Exception
	 */
	protected void makeExcel(String sheetName, int listCount) throws Exception {
		if (listCount > EXCEL_MAX_ROW * EXCEL_MAX_SHEET) {
			throw new Exception("생성할 수 있는 Excel sheet의 수(255개)를 넘었습니다.");
		}

		initWorkBook();
		createSheet(sheetName);
		makeHeaderRows();
		makeBodyRows();
		setSizeCells();
		mergeCells();
	}

	/**
	 * 최초의 workbook을 생성
	 * 
	 * @return HSSFWorkbook - POI workbook 객체
	 */
	protected HSSFWorkbook initWorkBook() {
		rowIndexAutoIncrease = -1;
		sheetIndexAutoIncrease = 0;
		workBook = new HSSFWorkbook();

		return workBook;
	}

	/**
	 * 특정 이름을 갖는 sheet를 생성
	 * 
	 * @param sheetName - String
	 * @return HSSFSheet - POI sheet 객체
	 */
	protected HSSFSheet createSheet(String sheetName) {
		rowIndexAutoIncrease = -1;
		currentSheet = workBook.createSheet(sheetName);

		return currentSheet;
	}

	/**
	 * 새로운 row를 생성합니다. 자동 생성되는 엑셀임을 감안하여 65535줄이 넘었을 경우 자동으로 새로운 sheet를<br>
	 * 생성하고 있습니다. 그러나 엑셀 2003 버전까지의 경우 최대 sheet가 255가 되면 더이상 생성이 불가능하므로<br>
	 * Exception을 발생합니다. 즉, 이 클래스에서 만들어주는 엑셀은 최대 65535 * 255 = 16,711,425 rows가
	 * 됩니다.<br>
	 * 
	 * @return HSSFRow - POI row 객체
	 * @throws Exception - java.lang.Exception
	 */
	protected HSSFRow createNextRow() throws Exception {
		rowIndexAutoIncrease++;

		if (rowIndexAutoIncrease >= EXCEL_MAX_ROW) {
			sheetIndexAutoIncrease++;
			createSheet("AUTO_CREATED_" + sheetIndexAutoIncrease);
			rowIndexAutoIncrease = 0;
		}

		currentRow = currentSheet.createRow(rowIndexAutoIncrease);
		return currentRow;
	}

	/**
	 * i번째 위치에 presetStyle 값에 해당하는 디자인을 갖는 새로운 셀을 생성
	 *  - pos는 0-base 기준입니다.
	 * 
	 * @param pos - int
	 * @param presetStyleConstant - int
	 * @param valueObject - Object
	 * @return HSSFCell
	 */
	protected HSSFCell createCell(int pos, int presetStyleConstant, Object valueObject) {
		int cellDataType = 0;
		HSSFCellStyle presetStyle = null;
		HSSFCell cell = createCell(pos, valueObject);

		if (valueObject == null) {
			presetStyleConstant = PRESET_STYLE_TEXT_NORMAL;
		}

		if (presetStyleConstant >= PRESET_STYLE_PERCENT_NORMAL) {
			cellDataType = HSSFCell.CELL_TYPE_NUMERIC;
			presetStyle = getPresetStyleNumber(presetStyleConstant);

		} else if (presetStyleConstant >= PRESET_STYLE_NUMBER_NORMAL) {
			cellDataType = HSSFCell.CELL_TYPE_NUMERIC;
			presetStyle = getPresetStyleNumber(presetStyleConstant);

		} else if (presetStyleConstant >= PRESET_STYLE_TEXT_NORMAL) {
			cellDataType = HSSFCell.CELL_TYPE_STRING;
			presetStyle = getPresetStyleText();

		} else if (presetStyleConstant == PRESET_STYLE_TEXT_HEADER) {
			cellDataType = HSSFCell.CELL_TYPE_STRING;
			presetStyle = getPresetStyleHeader();
		}

		cell.setCellType(cellDataType);
		cell.setCellStyle(presetStyle);

		return cell;
	}

	/**
	 * i번째 위치에 새로운 셀을 생성합니다. i는 0-base 기준입니다.
	 * 
	 * @param pos - int
	 * @param valueObject - Object
	 * @return HSSFCell
	 */
	protected HSSFCell createCell(int pos, Object valueObject) {
		HSSFCell cell = currentRow.createCell((short)pos);

		if (valueObject instanceof String) {
			cell.setCellValue((String)valueObject);
		} else if (valueObject instanceof Number) {
			double doubleValue = ((Number)valueObject).doubleValue();
			cell.setCellValue(doubleValue);
		} else if (valueObject instanceof Boolean) {
			cell.setCellValue((Boolean)valueObject);
		} else if (valueObject instanceof Calendar) {
			cell.setCellValue((Calendar)valueObject);
		} else if (valueObject instanceof Date) {
			cell.setCellValue((Date)valueObject);
		} else if (valueObject == null) {
			cell.setCellValue("");
		}

		return cell;
	}

	/**
	 * 시작점에서부터 mergeSize 만큼의 셀들을 가로로 병합니다. 
	 * sheet 인덱스와 좌표는 0-base로 시작합니다.
	 * 
	 * @param sheetIndex - int
	 * @param startRow - int
	 * @param startCol - int
	 * @param mergeSize - int
	 */
	protected void mergeHorizontalCells(int sheetIndex, int startRow, int startCol, int mergeSize) {
		mergeCells(sheetIndex, startRow, startCol, startRow, startCol + mergeSize - 1);
	}

	/**
	 * 시작점에서부터 mergeSize 만큼의 셀들을 세로로 병합니다. 
	 * sheet 인덱스와 좌표는 0-base로 시작합니다.
	 * 
	 * @param sheetIndex - int
	 * @param startRow - int
	 * @param startCol - int
	 * @param mergeSize - int
	 */
	protected void mergeVerticalCells(int sheetIndex, int startRow, int startCol, int mergeSize) {
		mergeCells(sheetIndex, startRow, startCol, startRow + mergeSize - 1, startCol);
	}

	/**
	 * 해당 좌표 안에 있는 모든 셀들을 하나의 셀로 병합합니다. 
	 * sheet 인덱스와 좌표는 0-base로 시작합니다.
	 * 
	 * @param sheetIndex - int
	 * @param rowFrom - int
	 * @param colFrom - int
	 * @param rowTo - int
	 * @param colTo - int
	 */
	protected void mergeCells(int sheetIndex, int rowFrom, int colFrom, int rowTo, int colTo) {
		HSSFSheet sheet = workBook.getSheetAt(sheetIndex);
		sheet.addMergedRegion(new Region(rowFrom, (short)colFrom, rowTo, (short)colTo));
	}

	/**
	 * colIndex에 해당하는 컬럼의 width를 정합니다. 
	 * colIndex은 0-base로 시작합니다.
	 * 
	 * @param colIndex - int
	 * @param size - int
	 * @param widthUnit - int
	 */
	protected void setColumnWidth(int colIndex, int size, int widthUnit) {
		short transformedSize = 0;
		HSSFSheet nextSheet;

		switch (widthUnit) {
			case WIDTH_UNIT_PIXEL:
				transformedSize = (short)(size * 1000 / 27);
				break;
			case WIDTH_UNIT_POI_DEFAULT:
				transformedSize = (short)size;
				break;
			default:
				transformedSize = (short)size;
				break;
		}

		for (int i = 0; i < workBook.getNumberOfSheets(); i++) {
			nextSheet = workBook.getSheetAt(i);
			nextSheet.setColumnWidth((short)colIndex, transformedSize);
		}
	}

	/**
	 * 해더 스타일을 생성
	 * 
	 * @return HSSFCellStyle
	 */
	protected HSSFCellStyle getPresetStyleHeader() {
		HSSFCellStyle style = workBook.createCellStyle();

		style.setBorderTop(HSSFCellStyle.BORDER_THIN);
		style.setBorderRight(HSSFCellStyle.BORDER_THIN);
		style.setBorderBottom(HSSFCellStyle.BORDER_THIN);
		style.setBorderLeft(HSSFCellStyle.BORDER_THIN);

		style.setTopBorderColor(HSSFColor.BLACK.index);
		style.setRightBorderColor(HSSFColor.BLACK.index);
		style.setBottomBorderColor(HSSFColor.BLACK.index);
		style.setLeftBorderColor(HSSFColor.BLACK.index);

		style.setFillForegroundColor(HSSFColor.GREY_25_PERCENT.index);
		style.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);

		style.setAlignment(HSSFCellStyle.ALIGN_CENTER);
		style.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);

		return style;
	}

	/**
	 * 텍스트 스타일을 생성<br>
	 * 
	 * @return HSSFCellStyle
	 */
	protected HSSFCellStyle getPresetStyleText() {
		HSSFCellStyle style = workBook.createCellStyle();

		style.setBorderTop(HSSFCellStyle.BORDER_THIN);
		style.setBorderRight(HSSFCellStyle.BORDER_THIN);
		style.setBorderBottom(HSSFCellStyle.BORDER_THIN);
		style.setBorderLeft(HSSFCellStyle.BORDER_THIN);

		style.setTopBorderColor(HSSFColor.BLACK.index);
		style.setRightBorderColor(HSSFColor.BLACK.index);
		style.setBottomBorderColor(HSSFColor.BLACK.index);
		style.setLeftBorderColor(HSSFColor.BLACK.index);

		style.setAlignment(HSSFCellStyle.ALIGN_LEFT);

		return style;
	}

	/**
	 * 숫자 스타일을 만듭니다.
	 * * Note. 백분율 스타일의 경우 Excel에 의하면 셀에 있는 값에 100을 곱한 값을 보여준다고 되어 있습니다.<br>
	 * 따라서 미리 100이 곱해져 있을 경우 값을 넣기 전에 100으로 나누어 주시기 바랍니다.<br>
	 * 
	 * @param styleConstant - int
	 * @return HSSFCellStyle
	 */
	protected HSSFCellStyle getPresetStyleNumber(int styleConstant) {
		HSSFCellStyle style = workBook.createCellStyle();

		style.setBorderTop(HSSFCellStyle.BORDER_THIN);
		style.setBorderRight(HSSFCellStyle.BORDER_THIN);
		style.setBorderBottom(HSSFCellStyle.BORDER_THIN);
		style.setBorderLeft(HSSFCellStyle.BORDER_THIN);

		style.setTopBorderColor(HSSFColor.BLACK.index);
		style.setRightBorderColor(HSSFColor.BLACK.index);
		style.setBottomBorderColor(HSSFColor.BLACK.index);
		style.setLeftBorderColor(HSSFColor.BLACK.index);

		style.setAlignment(HSSFCellStyle.ALIGN_RIGHT);

		String numberFormat = "";

		switch (styleConstant) {
			case PRESET_STYLE_NUMBER_NORMAL:
				numberFormat = "#,##0";
				break;

			case PRESET_STYLE_NUMBER_1DIGIT:
				numberFormat = "#,##0.0";
				break;

			case PRESET_STYLE_NUMBER_2DIGIT:
				numberFormat = "#,##0.00";
				break;

			case PRESET_STYLE_PERCENT_NORMAL:
				numberFormat = "0%";
				break;

			case PRESET_STYLE_PERCENT_2DIGIT:
				numberFormat = "0.00%";
				break;
		}

		style.setDataFormat(HSSFDataFormat.getBuiltinFormat(numberFormat));
		return style;
	}
}
